#include "sbrush_audio_manager.h"
#include "portaudio.h"
#include "dag_threading.h"

#include <string>
#include <set>
#include <map>
#include <iostream>
#include <fstream>
#include <queue>

using namespace std;
using namespace sbrush;
using namespace dag;

/* TODO: There are a couple issues with this file that should be handled 
 * eventually.  First off, it's a bit cluttered and difficult to maintain 
 * since different classes all call the same global functions and variables.
 * Also, I think having the deinterleave buffers as global objects is 
 * unnecessary.  Lastly, I think we're better off having the audio manager be
 * some sort of reference counted object that is freed when noone has a 
 * reference to it */

namespace {
	/* port audio stuff here */
	bool n_pa_is_initialized = false;
	PaStream* n_stream = NULL;
	int n_device_count = 0;
	
	map<int, string> n_device_names;
	map<int, int> n_device_max_inputs;
	map<int, int> n_device_max_outputs;
	map<int, double> n_device_default_samplerate;
	
	const char* n_audio_log_path = "sbrush_audio_log.txt";
	ofstream n_audio_log;
	
	/* audio manager variables */
	size_t n_num_inputs = 0;
	size_t n_num_outputs = 0;
	size_t n_max_frame_size = 0;
	float** n_parallel_out = NULL;
	float** n_parallel_in = NULL;
	double n_sample_rate = 0.0;
	
	set<AudioSource*> n_sources;
	Mutex n_source_mutex;
	
	void interleave(const float* const* in, 
					float* out, 
					size_t numChannels,
					size_t numFrames) {
		for (size_t frame = 0; frame < numFrames; frame++) {
			size_t offset = frame * numChannels;
			for (size_t channel = 0; channel < numChannels; channel++) {
				out[offset + channel] = in[channel][frame];
			}
		}
	}
	
	void deinterleave(const float* in,
					  float** out,
					  size_t numChannels,
					  size_t numFrames) {
		for (size_t frame = 0; frame < numFrames; frame++) {
			size_t offset = frame * numChannels;
			for (size_t channel = 0; channel < numChannels; channel++) {
				out[channel][frame] = in[offset + channel];
			}
		}
	}
	
	void dealloc_arrays(float**& arrays, size_t numChannels) {
		if (arrays != NULL) {
			for (size_t channel = 0; channel < numChannels; channel++) {
				delete[] arrays[channel];
				arrays[channel] = NULL;
			}
			delete[] arrays;
			arrays = NULL;
		}
	}
	
	float** alloc_arrays(size_t numChannels, size_t numFrames) {
		float** arrays = NULL;
		try {
			arrays = new float*[numChannels];
			memset(arrays, 0, sizeof(float*) * numChannels);
			for (size_t channel = 0; channel < numChannels; channel++) {
				arrays[channel] = new float[numFrames];
				memset(arrays[channel], 0, sizeof(float) * numFrames);
			}
		}
		catch (...) {
			dealloc_arrays(arrays, numChannels);
			throw;
		}
		return arrays;
	}
	
	
	int port_audio_callback(const void*input, 
							void *output, 
							unsigned long frameCount, 
							const PaStreamCallbackTimeInfo *timeInfo, 
							PaStreamCallbackFlags statusFlags, 
							void *userData) {
		if (frameCount > n_max_frame_size ||
			(n_num_outputs > 0 && (output == NULL || n_parallel_out == NULL))) {
			/* TODO: should log error or throw something? */
			return paAbort;
		}
		
		/* do audio callback */
		for (size_t channel = 0; channel < n_num_outputs; channel++) {
			memset(n_parallel_out[channel], 0, sizeof(float) * frameCount);
		}
		
		{
			ScopedLock sl(n_source_mutex);
			set<AudioSource*>::iterator asIter = n_sources.begin();
			while (asIter != n_sources.end()) {
				(*asIter)->addAudio(n_parallel_out,
								 n_num_outputs,
								 frameCount);
				asIter++;
			}
		}
		
		interleave(n_parallel_out, (float*)output, n_num_outputs, frameCount);
		/* TODO: allow for AudioSink */
		return paContinue;
	}
	
	/* manage audio sources.  performs adding and removing on it's own thread
	 * in order to avoid any deadlocks when trying to add/remove sources during
	 * the audio thread */
	class SourceManager : public Runnable {
		enum Action {
			Add,
			Remove
		};
		class Job {
		public:
			Job(AudioSource* s, Action a)
			:	source(s),
				action(a)
			{
			}
			
			AudioSource* source;
			Action action;
		};
	public:
		
		SourceManager()
		:	m_KeepRunning(true),
			m_IsRunning(false)
		{
		}
		
		~SourceManager() {
			stop();
			joinThread();
		}
		
		bool isRunning() {
			return m_IsRunning;
		}
		void runThread() {
			m_IsRunning = true;
			while (m_KeepRunning) {
				ScopedLock sl(m_AlterMutex);
				m_AlterCondition.wait(m_AlterMutex);
				{
					ScopedLock sl2(n_source_mutex);
					while (!m_JobQueue.empty()) {
						if (m_JobQueue.front().source != NULL) {
							if (m_JobQueue.front().action == Add) {
								n_sources.insert(m_JobQueue.front().source);
							} else if (m_JobQueue.front().action == Remove) {
								n_sources.erase(m_JobQueue.front().source);
							}
						}
						m_JobQueue.pop();
					}
				}
			}
			m_IsRunning = false;
		}
		
		void stop() {
			ScopedLock sl(m_AlterMutex);
			m_KeepRunning = false;
			m_AlterCondition.signal();
		}
		
		void addSource(AudioSource* source) {
			ScopedLock sl(m_AlterMutex);
			m_JobQueue.push(Job(source, Add));
			m_AlterCondition.signal();
		}
		
		void removeSource(AudioSource* source) {
			ScopedLock sl(m_AlterMutex);
			m_JobQueue.push(Job(source, Remove));
			m_AlterCondition.signal();
		}
	private:
		bool m_IsRunning;
		bool m_KeepRunning;
		queue<Job> m_JobQueue;
		Mutex m_AlterMutex;
		Condition m_AlterCondition;
	};
	
	/* class to ensure Pa_Terminate() is called */
	class PortAudioKiller {
	public:
		PortAudioKiller() {
		}
		
		void forceExist() {
		}
		
		~PortAudioKiller() {
			if (n_pa_is_initialized) {
				if (n_stream != NULL) {
					AudioManager::stop();
				}
				PaError err;
				err = Pa_Terminate();
				if (err != paNoError) {
					throw PortAudioError(err);
				}
			}
		}
	};
	
	PortAudioKiller n_pa_killer;
	SourceManager n_source_manager;
}

AudioManagerError::AudioManagerError(const string& msg)
:	AcsError(msg)
{
}

AudioManagerError::~AudioManagerError() throw() {
}

PortAudioError::PortAudioError(int errorCode) 
:	m_Code(errorCode),
	AudioManagerError(Pa_GetErrorText(errorCode))
{
}

PortAudioError::~PortAudioError() throw() {
}

int PortAudioError::code() const {
	return m_Code;
}

AudioSource::AudioSource() {
}

AudioSource::~AudioSource() throw() {
	AudioManager::removeSource(this);
}

void AudioManager::init() {
	if (!n_pa_is_initialized) {
		n_pa_killer.forceExist();
		/* initialize port audio */
		PaError err;
		err = Pa_Initialize();
		if (err != paNoError) {
			throw PortAudioError(err);
		}
		n_pa_is_initialized = true;
		
		/* get device info */
		n_device_count = Pa_GetDeviceCount();
		if (n_device_count < 0) {
			throw PortAudioError(n_device_count);
		}
		for (int device = 0; device < n_device_count; device++) {
			const PaDeviceInfo* info = Pa_GetDeviceInfo(device);
			if (info != NULL) {
				n_device_names[device] = string(info->name);
				n_device_max_inputs[device] = info->maxInputChannels;
				n_device_max_outputs[device] = info->maxOutputChannels;
				n_device_default_samplerate[device] = info->defaultSampleRate;
			}
		}
	}
}

void AudioManager::start(size_t numInputs, 
						 size_t numOutputs, 
						 double sampleRate, 
						 size_t frameSize,
						 int device) {
	if (!n_source_manager.isRunning()) {
		n_source_manager.startThread();
	}
	
	try {
		if (n_stream != NULL) {
			stop();
		}
		if (device == -1) {
			device = Pa_GetDefaultOutputDevice();
		}
		
		const PaDeviceInfo* info = Pa_GetDeviceInfo(device);
		if (info == NULL) {
			throw PortAudioError(paInvalidDevice);
		}
		
		PaStreamParameters inputParameters;
		inputParameters.device = device;
		inputParameters.channelCount = numInputs;
		inputParameters.sampleFormat = paFloat32;// | paNonInterleaved;
		inputParameters.suggestedLatency = info->defaultLowInputLatency;
		inputParameters.hostApiSpecificStreamInfo = NULL;
		
		PaStreamParameters outputParameters;
		outputParameters.device = device;
		outputParameters.channelCount = numOutputs;
		outputParameters.sampleFormat = paFloat32;// | paNonInterleaved;
		outputParameters.suggestedLatency = info->defaultLowOutputLatency;
		outputParameters.hostApiSpecificStreamInfo = NULL;
		
		PaStreamParameters *pop = NULL;
		PaStreamParameters *pip = NULL;
		
		if (numInputs > 0) {
			pip = &inputParameters;
		}
		if (numOutputs > 0) {
			pop = &outputParameters;
		}
		
		/* prepare deinterleaved buffers */
		dealloc_arrays(n_parallel_out, n_num_outputs);
		dealloc_arrays(n_parallel_in, n_num_inputs);
		if (numInputs > 0) {
			n_parallel_in = alloc_arrays(numInputs, frameSize);
		}
		if (numOutputs > 0) {
			n_parallel_out = alloc_arrays(numOutputs, frameSize);
		}
		n_max_frame_size = frameSize;
		
		PaError err;
		err = Pa_OpenStream(&n_stream, 
							pip, pop, 
							sampleRate, frameSize, 
							paNoFlag, 
							port_audio_callback, 
							NULL);
		if (err != paNoError) {
			throw PortAudioError(err);
		}
		
		n_num_inputs = numInputs;
		n_num_outputs = numOutputs;
		n_sample_rate = sampleRate;		
		
		err = Pa_StartStream(n_stream);
		if (err != paNoError) {
			throw PortAudioError(err);
		}

	}
	catch (...) {
		n_num_inputs = 0;
		n_num_outputs = 0;
		n_sample_rate = 0;
		throw;
	}
}

void AudioManager::stop() {
	dealloc_arrays(n_parallel_out, n_num_outputs);
	dealloc_arrays(n_parallel_in, n_num_inputs);
	if (n_stream != NULL) {
		PaError err;
		if (Pa_IsStreamActive(n_stream)) {
			err = Pa_StopStream(n_stream);
			if (err != paNoError) {
				throw PortAudioError(err);
			}
		}
		err = Pa_CloseStream(n_stream);
		if (err != paNoError) {
			throw PortAudioError(err);
		}
		n_stream = NULL;
	}
	if (n_source_manager.isRunning()) {
		n_source_manager.stop();
	}
}

void AudioManager::addSource(AudioSource* source) {
	if (source != NULL) {
		n_source_manager.addSource(source);
	}
}

void AudioManager::removeSource(AudioSource* source) {
	if (source != NULL) {
		n_source_manager.removeSource(source);
	}
}

size_t AudioManager::getNumInputs() {
	return n_num_inputs;
}

size_t AudioManager::getNumOutputs() {
	return n_num_outputs;
}

double AudioManager::getSampleRate() {
	return n_sample_rate;
}

size_t AudioManager::getNumDevices() {
	return n_device_count;
}

const char* AudioManager::getDeviceName(int device) {
	if (n_device_names.count(device)) {
		return n_device_names[device].c_str();
	}
	return "";
}

int AudioManager::getMaxInputChannels(int device) {
	if (n_device_max_inputs.count(device)) {
		return n_device_max_inputs[device];
	}
	return 0;
}

int AudioManager::getMaxOutputChannels(int device) {
	if (n_device_max_outputs.count(device)) {
		return n_device_max_outputs[device];
	}
	return 0;
}

double AudioManager::getDefaultSampleRate(int device) {	
	if (n_device_names.count(device)) {
		return n_device_default_samplerate[device];
	}
	return 0;
}

void AudioManager::makeNoiseAndLog(float** buffer, 
								   size_t numChannels, 
								   size_t numFrames) {
	for (size_t i = 0; i < numChannels; i++) {
		for (size_t j = 0; j < numFrames; j++) {
			if ((j % 100) < 50) {
				buffer[i][j] = 0.1f;
			}
			else {
				buffer[i][j] = -0.1f;
			}
		}
	}
	cout << "buffer filled with " << numFrames << " frames in " 
		<< numChannels << " channels." << endl;
}

void AudioManager::logAudio(float** buffer, 
							size_t numChannels, 
							size_t numFrames) {
	if (!n_audio_log.is_open()) {
		n_audio_log.open(n_audio_log_path, ios::trunc);
	}
	
	for (size_t frame = 0; frame < numFrames; frame++) {
		for (size_t channel = 0; channel < numChannels; channel++) {
			n_audio_log << buffer[channel][frame] << "\t";
		}
		n_audio_log << endl;
	}
}